前言
提起React,可能脑海里第一印象就是Virtual Dom, 性能好, 渲染快, 之前也看过很多现成的原理分析文章, 但是别人的东西始终是建立在别人的理解之上,
既然现在平时的工作已经大规模使用React及其生态,那么React 源码分析一方面可以加深对React理解,另外一方面学习其内核原理,更好的在工作业务中寻找一个最佳实践。
本次源码阅读的版本是 从2017年11.28的发布的 React 16.2版本
React16 的新特性
下面先了解一下React16的新特性
https://reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html#changes-in-detail
支持在html元素中传入 自定义属性
1 | <div tabIndex="-1" /> |
允许在render函数中返回节点数组,字符串
1 | // React 16.0 允许在render函数中返回节点数组 |
支持了 Fragment 标签
2017年11月28日最新发布的 React v16.2.0, 主要的更新点如下
1 | // 通常我们一个节点列表需要一个 div 或者spn 标签来包裹 |
Portals
1 | render() { |
主要的作用就是把组件插入到现有的dom节点中,这个特性对于jq的页面迁移到 react简直是福音.
比如当前有一个页面某个模块是jquery写的, 现在需要把其中某个模块迁移到react,
使用这个特性就非常方便, 就不用特地使用 ReactDom.render()方法单独挂载组件到 dom节点上
采用新的底层架构 Fiber
可以参考一下demo对比两个架构之间的差距:
- 老架构 stack https://claudiopro.github.io/react-fiber-vs-stack-demo/stack.html
- 新架构 fiber https://claudiopro.github.io/react-fiber-vs-stack-demo/fiber.html
更多细节可以参考官方的博客 https://reactjs.org
准备阶段
React的代码好几万行, 盲目的阅读有点大海捞针的感觉, 这里先思考几个问题, 我们总源码中找答案.
- Fiber引擎是什么, 它的原理是怎么样的
- functionComponent的实现
- 新特性中的 render 中return array是怎么实现的
- Fragment 实现
- virtual dom在 react中具体是以一个什么样的方式存在
现在要做的是:
- 去github下载一份源码 https://github.com/facebook/react/releases/tag/v16.2.0
- 下载一个好用的编辑器, 支持快速定位到当前function的引用
注意:
- 由于后续react升级, 本文所引用的部分代码可能会过期
- 为了优化阅读体验, 会删除部分非核心代码:)
导航
Fiber架构的调用关系链, 方便我们快速浏览整个react-fiber引擎的执行流程
帮助在阅读的过程中,可以快速定位到当前阅读模块在框架中的位置
看到下面一大堆函数 还有一张那么大的图,感觉无从下手?
那就直接进入主题: 进入主题
源码快速导航
Component:
ReactDOM.render,
Component,
React.createElement
legacyRenderSubtreeIntoContainer
enqueueSetState,
enqueueReplaceState,
enqueueForceUpdate
unbatchedUpdates,
batchedUpdates
updateContainerAtExpirationTime
scheduleRootUpdate,
insertUpdateIntoFiber
performWork:
performWork,
scheduleWorkImpl,
requestWork
findHighestPriorityRoot,
performWorkOnRoot
insertUpdateIntoFiber,
scheduleWorkImpl
Commit:
commitAllHostEffects,
commitAllLifeCycles
LifeCycles:
componentDidMount,
componentDidUpdate,
shouldComponentUpdate
componentWillUpdate
componentWillReceiveProps
workLoop:
workLoop,
performUnitOfWork,
beginWork
updateHostRoot,
updateClassComponent
reconcileChildren,
reconcileChildFibers,
reconcileChildrenArray
updateClassInstance,
constructClassInstance,
updateFunctionalComponent
completeWork,
finishClassComponent
DSL
下面是架构图的DSL代码, 可以复制代码到 https://yuml.me/ 生成 UML 图片, 也可以直接打开该页面 https://yuml.me/a3c07b49 编辑
1 | [ReactDOM.render{bg:wheat}] -Recursive Call> [render], |
源码阅读
React
首先来看一下react这个对象里面有啥1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// packages/react/src/React.js
import {Component, PureComponent, AsyncComponent} from './ReactBaseClasses';
import {forEach, map, count, toArray, only} from './ReactChildren';
import ReactCurrentOwner from './ReactCurrentOwner';
import {
createElement,
createFactory,
cloneElement,
isValidElement,
} from './ReactElement';
var React = {
Children: {
map,
forEach,
count,
toArray,
only,
},
Component,
PureComponent,
unstable_AsyncComponent: AsyncComponent,
Fragment: REACT_FRAGMENT_TYPE,
createElement: __DEV__ ? createElementWithValidation : createElement,
cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
isValidElement: isValidElement,
},
};
我们如果看babel编译过的react项目JSX代码, 通常我们一个下面是一个react的起点, 可以理解为 这个ReactDOM.render做的是吧react的对象和真实的浏览器dom节点关联
, JSX经过babel编译后,实际是调用createElement()创建实例对象
1 | ReactDOM.render(React.createElement(<Component/>, null), document.getElementById('portraits_root')); |
React.createElement 对象创建
我们先来分析一下 创建组件的入口 createElement
1 | /** |
上面的源码主要做三个工作 ,
- 把参数config 的挂到props变量里面
- 把 children传入到 props.children属性下
- 取出组件类中的静态变量defaultProps,并给未在JSX中设置值的属性设置默认值
- 返回一个 ReactElement
- 再看看 ReactElement的代码, 其实就是把把参数包装一下再加个 $$typeof标识返回
- 截止到这里, 可以看到createElement仅仅是返回一个 element对象
React.Children
React.Children提供了对this.props.children的操作的函数。
react/src/ReactChildren文件export的函数如下1
2
3
4
5
6
7export {
forEachChildren as forEach,
mapChildren as map,
countChildren as count,
onlyChild as only,
toArray,
};
Component
1 | function Component(props, context, updater) { |
上面的代码是输出三个类Component, PureComponent, AsyncComponent
- setState 实际上就是调用
this.updater.enqueueSetState(this, partialState, callback, ‘setState’); - forceUpdate实际上是调用
this.updater.enqueueForceUpdate(this, callback, ‘forceUpdate’);
这一块后面分析Fiber架构会详细深入了解 this.updater.enqueueSetState 和 this.updater.enqueueForceUpdate - PureComponent 和 asyncComponentPrototype 其实就是在Component的基础上分别设置
isPureReactComponent 和 unstable_isAsyncReactComponent 为true的熟悉, 这个标准在底层的架构中会判断这写标记做特殊判断, 比如 isPureReactComponent= true之后,组件在shoudComponentUpdate中会浅比较渲染前后的props和state,如果没有变化,组件不会进入接下来的生命周期,可以节省不必要的diff操作。
到此我们已经把React的对象分析的差不多了, 但是是不是有还是觉得不知道React是如何运行的? 下面进入React16新一代的架构Fiber 来进行探索
Fiber
分析前面的 React代码, 发现主要就是构造element对象, 那现在如何让整个react 引擎跑起来?
还记得前面的 ReactDOM.render(React.createElement(
ReactDOM.render
1 | // react/packages/react-dom/src/client/ReactDOM.js |
上面的render中 ,多次使用到 DOMRenderer 这个对象 ,
下面的我精简了很多function的细节, 只保留了名称, 从这些名称中可以看出来, 下面这些是 浏览器dom操作与react 核心的引擎的桥梁
同样的,DOM、Canvas、Native、VR、WebGL等等平台都有自己的renderer, 也就是说 fiber引擎可以对接不同平台的渲染接口
DOMRenderer
1 | const DOMRenderer = ReactFiberReconciler({ |
从上面看到其实DOMRenderer 对象是由ReactFiberReconciler返回的, 下面就真的进入到fiber的代码了
React-Reconciler
生看代码很苦涩, 还得结合理论描述来看
基础的概念:
协调算法(reconciliation)
协调: React用以比较两棵树的算法,其决定哪些部分需要更改。
更新(update): 即渲染React app的数据发生的一个改变。通常是setState的结果。最终会导致重新渲染。
调度(scheduling)
调度(scheduling): 决定事务什么时候执行的过程。
事务(work): 必须执行的计算。事务一般是由update引起的(比如setState)。
Fiber的主要目标是使React能够充分利用调度, 具体的需要实现:
- 暂停事务,在一段时间后再接着执行。
- 分配不同类型事务的优先级。
- 复用之前已完成的事务。
- 当事务不再需要时,终止该事务。
Fiber为了实现这些功能, 需要保存堆栈结构在内存里,然后在适当的时机去执行它。这点对于实现我们要的调度功能非常重要。
重要特性是增量渲染:将渲染事务分块并分布到多个帧去完成的能力,
关键特性包括有新的更新时,暂停、终止或重用事务的功能,为不同类型的更新设置优先级的功能.
以上几点概念理论引用自: React Fiber架构
下面可以详细看看 Fiber 引擎的代码,主要是指 React-Reconciler下面的代码, Reconciler(协调器) 就是所谓的 Virtul DOM
React组件更新渲染分为两个阶段:
- reconciliation 和 commit, Reconciler主要用于生成需要更新dom操作的列表,这个过程是 一个纯函数的过程, effectList = f(newProps, newState), 这个过程不会受到状态的变化而影响.
- 生成 effect list之后, commit effect list才会把变化应用到dom中, 这里需要调用render提供的dom操作接口方法, React-Dom React-Native 等都有提供接口给我Fiber
ReactFiberReconciler
1 | // react/packages/react-reconciler/src/ReactFiberReconciler.js |
ReactFiberReconciler 对外抛出的方法,
可以看到主要的几个方法都是ReactFiberScheduler返回的, 进一步证明了fiber架构在任务的调度上做了很大的改变
这些api主要是给 React-dom等 rendener的使用的
updateContainerAtExpirationTime
这个方法主要就是调用 scheduleRootUpdate
1 | function updateContainerAtExpirationTime( |
scheduleRootUpdate
1 | function scheduleRootUpdate( |
这个方法有顾名思义是开始调度根节点的update操作
主要分为三个操作:
- 生成一个update对象
- 把这个update 插入到 Fiber中
- 开始调度工作
看到这里开始接触到 Fiber对象了, 下面来了解一下 Fiber对象到底为何物
typeFiber
下面可以看看一个fiber的对象是怎么样了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39function FiberNode(
tag: TypeOfWork,
pendingProps: mixed,
key: null | string,
internalContextTag: TypeOfInternalContext,
) {
// Instance
this.tag = tag; // fiber的类型
this.key = key; // key是在协调算法中用来决定fiber是否可以重用的字段
this.type = null; // type描述了它对应的组件。对于复合组件,type就是复合函数或组件的class。对于宿主组件(div, span等),type是一个字符串。
this.stateNode = null;
// Fiber
this.return = null; // 指向fiber树中的父Fiber
this.child = null; // 子fiber(child fiber)指的是组件的render方法返回来的值。
this.sibling = null; // 兄弟fiber(sibling fiber)指代的是render方法返回多个子节点的情况(Fiber中的新特性!), 兄弟fiber形成一个单链表
this.index = 0;
this.ref = null;
// 一个fiber的pendingProps会在它开始执行处设置,memoizedProps则会在执行结尾处设置。
// 当到来的pendingProps和上一个memoizedProps相等时,它意味着fiber的上一次输出可以重用,避免不必要的事务。
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null; // 待更新update队列
this.memoizedState = null;
this.internalContextTag = internalContextTag;
// Effects
this.effectTag = NoEffect; // side effect类型
this.nextEffect = null; // 单链表结构,方便遍历fiber树上有副作用的节点
this.firstEffect = null;
this.lastEffect = null;
this.expirationTime = NoWork;
this.alternate = null; // 在fiber更新时克隆出的镜像fiber,对fiber的修改会标记在这个fiber上, 这个就是 源码中经常出现的working-in-progres
}
insertUpdateIntoFiber
1 | export function insertUpdateIntoFiber<State>( |
fiber.updateQueue是一个单向链表,有first和last指针指向链表的头部和尾部。
一个React element会有一个current fiber和一个alternate fiber。alternate fiber又叫working in progress fiber。
这两个fiber都有一个Update Queue。
这两个Queue里面的item的引用是相同的. 区别在于,working in progress fiber会在更新完一个队列项之后将其从队列中移除。
所以working in progress update queue永远是current queue的一个子集。
在更新完成之后,working in progress fiber取代current fiber成为新的current fiber。如果更新中断(有更高优先级的更新插入),
current fiber的update queue就可以作为备份,使得之前中断的更新可以重新开始。
总之, insertUpdateIntoFiber,这个函数处理了将一个update插入到fiber.updateQueue和fiber.alternateFiber.updateQueue两个队列中操作
scheduleWorkImpl
1 | function scheduleWorkImpl( |
requestWork
1 | function requestWork(root: FiberRoot, expirationTime: ExpirationTime) { |
requestWork主要做3件事:
- 把 root 节点添加到已调度的队列中去
- 如果已经在调度的队列中, 则设置 expirationTime (这里的截止时间就是任务执行的优先级)
- 调用performWorkOnRoot 或者 performWork来执行更新队列
performWorkOnRoot
1 | function performWorkOnRoot( |
performWorkOnRoot 主要区分 同步渲染和异步渲染, 调用 renderRoot , completeRoot来执行任务
异步渲染会先判断一下 shouldYield()为false才 执行completeRoot, 也就是说异步渲染会先计算effect list, 但是不 commit effect,
然后根据当前周期剩余的时间来决定是否执行commiting操作
renderRoot
1 | function renderRoot( |
执行的步骤:
- 检测是否有上一次未完成的任务, 如果有, 就回复上个堆栈继续执行
- 执行 workLoop
workLoop
1 | function workLoop(expirationTime: ExpirationTime) { |
performUnitOfWork
1 | function performUnitOfWork(workInProgress: Fiber): Fiber | null { |
- beginWork, 异步情况会返回一个workLoop里面whild循环中的 nextUnitOfWork,
- 如果返回null, 就 completeUnitOfWork ()
一般情况一个react中的事务分为 begin 和 complete 两步, 下面先看 beginWork 的部分
beginWork
1 | function beginWork( |
beginWork函数是一个入口, 根据fiber节点不同的tag,调用对应的update方法。
比如下面可以里先看看 updateClassComponent 这个方法
updateClassComponent
1 | function updateClassComponent( |
updateClassComponent 主要判断是否初始化了ClassComponent, 如果没有就constructClassInstance一个实例, mountClassInstance然后挂载实例
如果有实例, 有就执行updateClassInstance, 主要返回 shouldUpdate
最后返回 finishClassComponent() 的结果
updateClassInstance
1 | function updateClassInstance( |
这个updateClassInstance主要的工作:
- 当前 instance 的 props, state, context备份为老的版本, 比如 oldProps = workInProgress.memoizedProps; newProps = workInProgress.pendingProps;
- 调用 callComponentWillReceiveProps
- 遍历workInProgress.updateQueue, 执行update, 生成newState
- checkShouldComponentUpdate, 这个地方会调用 ShouldComponentUpdate这个钩子函数
- 如果shouldUpdate 为true, 调用componentWillUpdate钩子函数
- 不管shouldUpdate是什么, 都更新当前 instance 的 props, state, context, 这样子这个work的相当于可以被复用了, 就算没有rerender, 至少更新了instance内部的状态
updateFunctionalComponent
1 | function updateFunctionalComponent(current, workInProgress) { |
finishClassComponent
1 | function finishClassComponent( |
reconcileChildrenAtExpirationTime
1 | function reconcileChildrenAtExpirationTime( |
这个方法主要判断当前的节点是否已经挂载并且render过了,
没有挂载: 执行mountChildFibers, 不会做side-effect 的优化, 会直接render这个节点
已经挂载: 执行reconcileChildFibers, 会在virtual-dom中 使用算法优化, 生成最小的 side-effects, 用最小的dom变更去优化渲染性能
reconcileChildFibers
下面也能揭晓最开始 说的react新特性 支持 reder中return数组和字符串
总的来说,这个函数根据newChild的类型调用不同的方法。
newChild可能是一个元素,也可能是一个数组(React16新特性)
如果是reconcile单个元素,以reconcileSingleElement为例比较key和type,如果相同,复用fiber,删除多余的元素(currentFirstChild的sibling),
如果不同,调用createFiberFromElement,返回新创建的。
如果是string,reconcileSingleTextNode
如果是array,reconcileChildrenArray
如果是空,deleteRemainingChildren删除老的子元素
1 | // This API will tag the children with the side-effect of the reconciliation |
reconcileChildrenArray
react最有名的virtual-dom 的diff过程可以用下面的这个方法来表示1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
expirationTime: ExpirationTime,
): Fiber | null {
// This algorithm can't optimize by searching from boths ends since we
// don't have backpointers on fibers. I'm trying to see how far we can get
// with that model. If it ends up not being worth the tradeoffs, we can
// add it later.
// Even with a two ended optimization, we'd want to optimize for the case
// where there are few changes and brute force the comparison instead of
// going for the Map. It'd like to explore hitting that path first in
// forward-only mode and only go for the Map once we notice that we need
// lots of look ahead. This doesn't handle reversal as well as two ended
// search but that's unusual. Besides, for the two ended optimization to
// work on Iterables, we'd need to copy the whole set.
// In this first iteration, we'll just live with hitting the bad case
// (adding everything to a Map) in for every insert/move.
// If you change this code, also update reconcileChildrenIterator() which
// uses the same algorithm.
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
// 遍历一遍 新的newChildren数组, 通过 updateSlot来对比 新老数组的元素, 如果是相同元素, 就更新 fiber链表
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
expirationTime,
);
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
// a better way to communicate whether this was a miss or null,
// boolean, undefined, etc.
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// We matched the slot, but we didn't reuse the existing fiber, so we
// need to delete the existing child.
deleteChild(returnFiber, oldFiber);
}
}
// fiber是一个链表的数据结构, 这一步操作就是把 newFiber 放到链表的正确的位置, 构建 fiber 链表
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
// TODO: Defer siblings if we're not at the right index for this slot.
// I.e. if we had null values before, then we want to defer this
// for each null value. However, we also don't want to call updateSlot
// with the previous one.
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// 如果遍历完新数组, 把老数组剩下的元素都删除了
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 如果老的数组已经遍历结束了, 但是新的数组没有结束, 遍历一下新数组剩下元素然后插入
if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
expirationTime,
);
if (!newFiber) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// Add all children to a key map for quick lookups.
// 如果没有命中上面两中情况: 那就是发生了移动或者删除的操作, 就把所有老数组元素按key放map里
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// Keep scanning and use the map to restore deleted items as moves.
// 继续遍历新的数组, 把老数组中有用的元素的插入到新的数组中,
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
expirationTime,
);
// 如果老数组这里面这个元素在新数组中有, 就插入,到新数组中, 并且在existingChildren 把这个元素删除, 因为最后existingChildren 中的元素都会被删除
if (newFiber) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// The new fiber is a work in progress, but if there exists a
// current, that means that we reused the fiber. We need to delete
// it from the child list so that we don't add it to the deletion
// list.
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
// 删除没有命中上面情况的元素, 就是删除老的数组中 非(老数组 交集 新数组)的元素
if (shouldTrackSideEffects) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
最后返回的是一个fiber链表的第一个元素
return resultingFirstChild;
}
diff完成之后, 一直return, workInProgress.child = resultingFirstChild,
后面回到 performUnitOfWork函数中, 执行 completeUnitOfWork
completeUnitOfWork
1 | function completeUnitOfWork(workInProgress: Fiber): Fiber | null { |
函数主主体是一个循环, 主要的功能就是调用 completeWork, 同时也执行 把 子树的 effect list 插入到 effect list of the parent.
completeWork
1 | function completeWork( |
这个方法主要的根据fiber的 tag 来执行不同的 update
我们可以分析其中的常见的 tag 为 HostComponent的情况,可以看到 调用 host 传入的 prepareUpdate 方法, 生成需要更新的dom属性, 然后执行updateHostComponent方法
prepareUpdate 和 updateHostComponent 都是react-dom里面传入的方法, 同样 在react-native, canvas等其他平台也有同样的定义, 相当于是一个规范的接口来兼容不同的硬件层
updateHostComponent
1 | updateHostComponent = function( |
截止到这里, reconcile的操作已经结束, react的两个阶段(reconciliation , commiting) 已经完成了第一步
现在要回到 performWorkOnRoot, 开始执行 completeRoot
completeRoot
1 | function completeRoot( |
这个代码先判断一下是不是满足批量操作的优先级, 如果是满足, 就继续等下一个update, 最后一起批量地update
不然就调用commitRoot来提交更新
commitRoot
1 | //提交effect list更新到 dom中 |
commitRoot 主要分为三步
- prepareForCommit() 调用HostConfig.prepareForCommit的方法, 生成待提交的dom属性变更
- 遍历effect list执行 commitAllHostEffects(), commit 所有的 effect,
- 遍历 effect list 执行 commitAllLifeCycles(), commit 所有的生命周期, 就是执行 componentDidupdate之类的方法
commitAllHostEffects
// 这个地方就是把所有的 effect都提交了,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64function commitAllHostEffects() {
// 把所有的effect 都遍历一遍, 然后
while (nextEffect !== null) {
if (__DEV__) {
ReactDebugCurrentFiber.setCurrentFiber(nextEffect);
}
recordEffect();
const effectTag = nextEffect.effectTag;
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
// The following switch statement is only concerned about placement,
// updates, and deletions. To avoid needing to add a case for every
// possible bitmap value, we remove the secondary effects from the
// effect tag and switch on that value.
let primaryEffectTag =
effectTag & ~(Callback | Err | ContentReset | Ref | PerformedWork);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is inserted, before
// any life-cycles like componentDidMount gets called.
// TODO: findDOMNode doesn't rely on this any more but isMounted
// does and isMounted is deprecated anyway so we should be able
// to kill this.
nextEffect.effectTag &= ~Placement;
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is inserted, before
// any life-cycles like componentDidMount gets called.
nextEffect.effectTag &= ~Placement;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
isUnmounting = true;
commitDeletion(nextEffect);
isUnmounting = false;
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
commitWork
1 | function commitWork(current: Fiber | null, finishedWork: Fiber): void { |
清空当前fiber的 updateQueue
根据tag选择HostConfig中的 相关的 commit 方法, 比如当 tag = HostComponent , 调用 commitUpdate方法, 提交dom更新
commitAllLifeCycles
1 | function commitAllLifeCycles() { |
一个循环遍历 effect-list, 调用 commitLifeCycles 来触发相关的钩子函数
commitLifeCycles
1 | function commitLifeCycles(current: Fiber | null, finishedWork: Fiber): void { |
这个方法主要就是根据不同的fiber.tag 来处理组件生命周期
比如当tag=ClassComponent的时候, 把当前fiber中缓存的 props state赋值给组件实例, 并且调用componentDidUpdate钩子函数来完成一次组件更新
回顾问题
让我们来看看有没有解决最开始思考的问题
- Fiber引擎是什么, 它的原理是怎么样的: Fiber
- functionComponent的实现: updateFunctionalComponent
- 新特性中的 render 中return array是怎么实现的: reconcileChildFibers
- Fragment 实现 beginWork -> react/packages/react-reconciler/src/ReactFiberBeginWork.js -> updateFragment
- virtual dom在 react中具体是以一个什么样的方式存在: reconcileChildrenArray
在阅读的过程中, 也思考了新的问题:
- 调度器怎么样找到下一个要执行的事务单元?
- 优先级怎么设置的?
- 调度器怎么样知道什么时候暂停和继续事务?
- 事务是怎么执行和标记为完成的? (事务分为两个步骤, begin complete,complete操作可以延期执行)
- 生命周期是怎么样被调用的?
返回到 导航, 再看一遍框架图, 可能会有不一样的感觉吧!
结束
对于React16的Fiber引擎分析到这里的结束了, 手动撒花!!!!
当前文章的代码解读是基于 17年11月28发布的 React 16.2,因为时间和个人经验有限, 难免有理解错误或者不足的地方, 希望多多指正.
鉴于当前文章篇幅过长, 后续还将继续分析React-Dom相关的, 这一块主要就是 renderer的的工作
- React-Dom/events
- React-Dom/client